<?php

namespace Fluency\Web\Http;

use Fluency\Web\RequestDispatcher;

/**
 * An immutable representation of a request for a specific action. It encapsulates the action name,
 * the request parameters (a name-value mapping), the requested content type, the scope of the
 * request (either internal or external) and a reference to a RequestDispatcher instance which
 * is used to execute the request.
 *
 * @package Fluency\Web\Http
 * @author  Ignas Rudaitis
 * @since   1.0
 */
class Request
{
    private $dispatcher;
    private $action = "";
    private $params = array();
    private $format = "";
    private $internal = false;

    /**
     * The constructor. It serves as a copy constructor if the optional second Request argument
     * is provided.
     *
     * @param Fluency\Web\RequestDispatcher $dispatcher the RequestDispatcher that will be used to
     *        dispatch the request
     * @param Fluency\Web\Http\Request $source another instance to copy fields from
     */
    public function __construct(RequestDispatcher $dispatcher, Request $source = null)
    {
        if ($source !== null) {
            $this->action = $source->action;
            $this->params = $source->params;
            $this->format = $source->format;
            $this->internal = $source->internal;
        }
        $this->dispatcher = $dispatcher;
    }

    /**
     * Gets the name of the requested action.
     *
     * @return string
     */
    public function getAction()
    {
        return $this->action;
    }

    /**
     * Returns a clone of the current instance with the name of the requested action set to the
     * specified value.
     *
     * @param string $action the action name
     * @return Fluency\Web\Http\Request
     */
    public function forAction($action)
    {
        $clone = clone $this;
        $clone->action = $action;
        return $clone;
    }

    /**
     * Gets the value of a request parameter with the specified name.
     *
     * @throws OutOfBoundsException if no parameter with the specified name exists and no default
     *         value is provided
     * @param string $name the name of the parameter
     * @param mixed $default the value to return if no parameter with the specified name exists
     * @return mixed
     */
    public function getParam($name, $default = null)
    {
        if (isset($this->params[$name])) {
            return $this->params[$name];
        } elseif (func_num_args() == 2) {
            return $default;
        } else {
            throw new \OutOfBoundsException("Request parameter named '$name' does not exist");
        }
    }

    /**
     * Gets a key-value mapping of all request parameters.
     *
     * @return mixed[]
     */
    public function getAllParams()
    {
        return $this->params;
    }

    /**
     * Returns a clone of the current instance with the request parameter with the given name set
     * to the given value.
     *
     * @param string $name
     * @param mixed $value
     * @return Fluency\Web\Http\Request
     */
    public function withParam($name, $value)
    {
        $clone = clone $this;
        $clone->params[$name] = $value;
        return $clone;
    }

    /**
     * Returns a clone of the current instance with the request parameter mapping merged with the
     * given one. Parameters with the same names are overwritten.
     *
     * @param mixed[] $params
     * @return Fluency\Web\Http\Request
     */
    public function withParams(array $params)
    {
        $clone = clone $this;
        $clone->params = $params + $this->params;
        return $clone;
    }

    /**
     * Returns a clone of the current instance with all request parameters deleted.
     *
     * @return Fluency\Web\Http\Request
     */
    public function withoutParams()
    {
        $clone = clone $this;
        $clone->params = array();
        return $clone;
    }

    /**
     * Gets the requested content format.
     *
     * @return string
     */
    public function getFormat()
    {
        return $this->format;
    }

    /**
     * Returns a clone of the current instance with the content format set to the given one.
     *
     * @param string $format
     * @return Fluency\Web\Http\Request
     */
    public function withFormat($format)
    {
        $clone = clone $this;
        $clone->format = $format;
        return $clone;
    }

    /**
     * Checks if the request has internal scope.
     *
     * @return boolean
     */
    public function isInternal()
    {
        return $this->internal;
    }

    /**
     * Checks if the request has external scope.
     *
     * @return boolean
     */
    public function isExternal()
    {
        return !$this->internal;
    }

    /**
     * Returns a clone of the current instance with internal scope.
     *
     * @return Fluency\Web\Http\Request
     */
    public function withInternalScope()
    {
        $clone = clone $this;
        $clone->internal = true;
        return $clone;
    }

    /**
     * Returns a clone of the current instance with external scope.
     *
     * @return Fluency\Web\Http\Request
     */
    public function withExternalScope()
    {
        $clone = clone $this;
        $clone->internal = false;
        return $clone;
    }

    /**
     * Gets the instance of RequestDispatcher associated with this request.
     *
     * @return Fluency\Web\RequestDispatcher
     */
    public function getDispatcher()
    {
        return $this->dispatcher;
    }

    /**
     * Returns a new Request object with the same RequestDispatcher instance associated with it.
     *
     * @return Fluency\Web\Http\Request
     */
    public function createChildRequest()
    {
        return new self($this->dispatcher);
    }

    /**
     * Executes the request using the associated instance of RequestDispatcher and returns the
     * response if possible.
     *
     * @return Fluency\Web\Http\Response
     */
    public function execute()
    {
        return $this->dispatcher->dispatch($this);
    }

    /**
     * Checks if the associated instance of RequestDispatcher can dispatch the request.
     *
     * @return boolean
     */
    public function isExecutable()
    {
        return $this->dispatcher->canDispatch($this);
    }
}
